package org.melanesia.sql;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.melanesia.beans.BeanUtils;
import org.melanesia.conf.Config;
import org.melanesia.converters.JavaToJdbcConverter;
import org.melanesia.converters.TypeConverter;
import org.melanesia.ref.ReferenceMonitor;
import org.melanesia.sql.exceptions.SQLExecutionException;
import org.melanesia.sql.utils.JDBCUtils;

/**
 * Query class allows for sql-query execution.
 *
 * @author marcin.kielar
 */
public final class Query extends AbstractSQL {

    /** Query medadata. */
    private final QueryMetaData queryMetaData = new QueryMetaData();

    /**
     * Intermediate property classes, for instantiating intermediate properties.
     */
    private final Map<String, Class<?>> itermediatePropertyClasses = new HashMap<String, Class<?>>();

    /**
     * Constructs new <code>Query</code>.
     *
     * @param connection
     *            jdbc connection
     * @param typeConverter
     *            type converter
     * @param sql
     *            sql clause
     */
    Query(final Connection connection, final TypeConverter typeConverter, final String sql) {
        super(connection, typeConverter, sql);
    }

    /**
     * Sets the query parameter value.
     *
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     * @return this query
     */
    public Query setParameter(final String name, final Object value) {
        queryMetaData.setInputParameter(name, value);
        return this;
    }

    /**
     * Sets the query parameter value and explicitly defines java do jdbc
     * converter.
     *
     * @param name
     *            parameter name
     * @param value
     *            parameter value
     * @param converter
     *            java to jdbc converter
     * @return this query
     */
    public Query setParameter(final String name, final Object value, final JavaToJdbcConverter converter) {
        queryMetaData.setInputParameter(name, value, converter);
        return this;
    }

    /**
     * See {@link QueryMetaData#setPropertyPathForResult(String, String)}.
     *
     * @param columnName
     *            column name
     * @param propertyPath
     *            property path in result java bean
     * @return this query
     */
    public Query setPropertyPathForColumn(final String columnName, final String propertyPath) {
        queryMetaData.setPropertyPathForResult(columnName, propertyPath);
        return this;
    }

    /**
     * Sets class for a particular bean property. This is mainly used with
     * composite properties. The idea of composite properties is discussed in
     * {@link org.melanesia.beans.Setter#set(Object, Object, Map)}.
     *
     * @param bean
     *            name of the beanProperty used in SQL statement. for example:
     *            "b", "b.c", "b.c.d"
     * @param beanClass
     *            class to be used when instantiating the bean
     * @return this query
     */
    public Query setIntermediatePropertyClass(final String bean, final Class<?> beanClass) {
        itermediatePropertyClasses.put(bean, beanClass);
        return this;
    }

    /**
     * Executes the query with no result returned.
     */
    public void execute() {
        executeInternal(null, 0, null);
    }

    /**
     * Executes the query returning list of beans of class
     * <code>returnedBeanClass</code>. Each row from the result set is mapped
     * into single instance of <code>returnedBeanClass</code>.
     *
     * @param <T>
     *            returned type
     * @param returnedBeanClass
     *            returned class
     * @return list of beans representing the query result set
     */
    public <T> List<T> execute(final Class<T> returnedBeanClass) {
        return executeInternal(returnedBeanClass, 0, null);
    }

    /**
     * Executes the query returning the list of only the values of the selected
     * column.
     *
     * NOTE! This method SHOULD be given an Object type (not a primitive type)
     * in the <code>returnedClass</code>.
     *
     * If a primitive type is given (i.e. int.class, boolean.class), and there
     * is no result, or the result is null, this method will cause an
     * {@link org.melanesia.beans.exceptions.NullCastToPrimitiveTypeException},
     * as described in {@link org.melanesia.beans.BeanUtils#cast(Object, Class)}
     *
     * The user MAY pass a primitive type if she/he is sure that there will
     * always be a not null result or if she/he is aware of the
     * {@link org.melanesia.beans.exceptions.NullCastToPrimitiveTypeException}
     * and handles it in user code.
     *
     * It is however advised not to use primitive types at all with this method.
     *
     * @param <T>
     *            return type
     * @param returnedClass
     *            type of returned values
     * @param returnColumnName
     *            column name from result set, for which value will be returned
     * @return list of values for given <code>returnColumnName</code>
     */
    public <T> List<T> execute(final Class<T> returnedClass, final String returnColumnName) {
        return executeInternal(returnedClass, 0, returnColumnName);
    }

    /**
     * Executes the query returning a bean of class
     * <code>returnedBeanClass</code> representing the first row of the result
     * set. This method should be used primarily in cases, when it is
     * guaranteed, that the query returns only one row, or it is desired to
     * process only the first row of a result set.
     *
     * NOTE! For optimization, it is advised to use "AND ROWNUM = 1", or similar
     * mechanism guaranteeing the return of a single row.
     *
     * @param <T>
     *            returned type
     * @param returnedBeanClass
     *            returned class
     * @return list of beans representing the query result set
     */
    public <T> T executeSingle(final Class<T> returnedBeanClass) {

        List<T> list = executeInternal(returnedBeanClass, 1, null);
        if (!list.isEmpty()) {
            return list.get(0);
        } else {
            return null;
        }
    }

    /**
     * Executes the query returning only the value of the selected column from
     * first row of the result set. This method should be used primarily in
     * cases, when it is guaranteed, that the query returns only one row, or it
     * is desired to process only the first row of a result set.
     *
     * NOTE! For optimization, it is advised to use "AND ROWNUM = 1", or similar
     * mechanism guaranteeing the return of a single row.
     *
     * NOTE! This method SHOULD be given an Object type (not a primitive type)
     * in the <code>returnedClass</code>.
     *
     * If a primitive type is given (i.e. int.class, boolean.class), and there
     * is no result, or the result is null, this method will cause an
     * {@link org.melanesia.beans.exceptions.NullCastToPrimitiveTypeException},
     * as described in {@link org.melanesia.beans.BeanUtils#cast(Object, Class)}
     *
     * The user MAY pass a primitive type if she/he is sure that there will
     * always be a not null result or if she/he is aware of the
     * {@link org.melanesia.beans.exceptions.NullCastToPrimitiveTypeException}
     * and handles it in user code.
     *
     * It is however advised not to use primitive types at all with this method.
     *
     * @param <T>
     *            object type of returned value
     * @param returnedClass
     *            returned class
     * @param returnColumnName
     *            name of the column from result set to return
     * @return value of the column specified in <code>returnColumnName</code>
     */
    public <T> T executeSingle(final Class<T> returnedClass, final String returnColumnName) {

        List<T> list = executeInternal(returnedClass, 1, returnColumnName);
        if (!list.isEmpty()) {
            return list.get(0);
        } else {
            return null;
        }
    }

    /**
     * Works similar to {@link #execute(Class)}, except it returns an
     * {@link Iterator} instead of the list of results. The difference is this
     * method does not fetch all the rows of the result set immediately. The
     * rows are fetched (and mapped to resulting beans) only when
     * {@link Iterator#next} is called.
     *
     * This approach was designed to minimize memory usage - when the rows of
     * the result set are iterated, only a single bean exists at specific moment
     * of time.
     *
     * @param <T>
     *            return type
     * @param returnedClass
     *            returned class
     * @return result set iterator
     */
    public <T> Iterator<T> iterator(final Class<T> returnedClass) {
        return iterator(returnedClass, null);
    }

    /**
     * This method is similar to {@link #iterator(Class)} except that it does
     * not map the whole result set to a result bean, but returns a list of only
     * the single column values. The column name is specified by
     * {@code returnColumnName}.
     *
     * @param <T>
     *            return type
     * @param returnedClass
     *            returned class
     * @param returnColumnName
     *            column name from result set, for which value will be returned
     * @return result set iterator accessing single column
     */
    public <T> Iterator<T> iterator(final Class<T> returnedClass, final String returnColumnName) {
        try {
            return createIterator(returnedClass, returnColumnName, 0);
        } catch (Throwable t) {
            throw new SQLExecutionException(t);
        }
    }

    /**
     * Internal method for executing queries.
     *
     * @param <T>
     *            return type
     * @param returnedClass
     *            return type
     * @param maxFetchSize
     *            maximum fetch size - callers should set this value depending
     *            on expected / needed size of the result set
     * @param returnColumnName
     *            name of the column, for single column retrieval
     * @return list of query results
     */
    private <T> List<T> executeInternal(final Class<T> returnedClass, final int maxFetchSize, final String returnColumnName) {

        ArrayList<T> result = new ArrayList<T>();
        Iterator<T> iterator = null;
        try {

            iterator = createIterator(returnedClass, returnColumnName, maxFetchSize);

            if (returnedClass != null) {

                int cnt = 0;

                while (iterator.hasNext() && (maxFetchSize <= 0 || maxFetchSize > cnt)) {
                    T val = iterator.next();
                    result.add(val);

                    ++cnt;
                }

                if (cnt == 0 && returnColumnName != null) {
                    // We have a specified output parameter to return,
                    // but query returned no result. We have to return something
                    // anyway, so try null binding to be sure it works.
                    T acceptedValue = bindResultToSimpleValue(returnedClass, returnColumnName, null);

                    result.add(acceptedValue);
                }
            }

            return result;
        } catch (SQLExecutionException ex) {
            throw ex;
        } catch (Throwable t) {
            throw new SQLExecutionException(t);
        } finally {
            closeIterator(iterator);
        }
    }

    /**
     * Iterator wrapper for a result set. This class implements the
     * {@link Iterator} interface, but extends it with a
     * {@link Iterator#close()} method. The {@link Iterator#close()} should be
     * called always when the {@code Iterator} will no longer be used, as it
     * closes internal {@link ResultSet} and {@link CallableStatement}.
     *
     * Note, that, the {@code Iterator} is automatically closed when all of the
     * internal result set rows are returned using {@link #next} method (so the
     * {@link #hasNext()} method returns false).
     *
     * @author marcin.kielar
     *
     * @param <T>
     *            value class for this iterator
     */
    public final class Iterator<T> implements java.util.Iterator<T> {

        /** The statement. */
        private final CallableStatement statement;

        /** The result set. */
        private final ResultSet resultSet;

        /** The result set metadata. */
        private final ResultSetMetaData resultSetMetadata;

        /** The bean class to map results to. */
        private final Class<T> returnedClass;

        /** A column name from result set, which value is to be returned. */
        private final String returnColumnName;

        /** A flag indicating there are more records in the iterator. */
        private Boolean hasNext = null;

        /**
         * Constructs new {@code Iterator}.
         *
         * @param statement
         *            the sql statement
         * @param resultSet
         *            the result set
         * @param returnedClass
         *            the bean class to map results to
         * @param returnColumnName
         *            a column name from result set, which value is to be
         *            returned
         * @throws SQLException
         *             if getting metadata from result set fails
         */
        Iterator(final CallableStatement statement, final ResultSet resultSet, final Class<T> returnedClass,
                final String returnColumnName) throws SQLException {
            this.statement = statement;
            this.resultSet = resultSet;
            this.resultSetMetadata = resultSet.getMetaData();
            this.returnedClass = returnedClass;
            this.returnColumnName = returnColumnName;
        }

        /**
         * Closes this result iterator.
         */
        public void close() {
            JDBCUtils.close(resultSet);
            JDBCUtils.close(statement);
        }

        /**
         * Moves the internal result set forward by one row.
         */
        private void doNext() {
            try {
                hasNext = resultSet.next();
                if (!hasNext) {
                    close();
                }
            } catch (Throwable t) {
                throw new SQLExecutionException(t);
            }
        }

        @Override
        public boolean hasNext() {

            if (hasNext == null) {
                doNext();
            }

            return hasNext;
        }

        @Override
        public T next() {

            try {
                // If hasNext has not been initialized, that means we are before
                // first row of the resultSet.
                // So we move the resultSet to the first row.
                if (hasNext == null) {
                    doNext();
                }

                // Fill the bean
                if (hasNext) {
                    T bean = null;
                    if (returnColumnName != null) {

                        // We have a specified output parameter to return,
                        // and ignore the rest.
                        Object returnColumnValue = resultSet.getObject(returnColumnName);
                        bean = bindResultToSimpleValue(returnedClass, returnColumnName, returnColumnValue);

                    } else {

                        bean = BeanUtils.newInstance(returnedClass);

                        for (int i = 1, size = resultSetMetadata.getColumnCount(); i <= size; i++) {

                            String columnLabel = resultSetMetadata.getColumnLabel(i);

                            Object resultValue = resultSet.getObject(columnLabel);
                            String resultName = queryMetaData.getPropertyPathForResult(columnLabel);

                            bindResultToBean(bean, resultName, resultValue, itermediatePropertyClasses);
                        }
                    }

                    // Move the resultSet, to next row, so next call to this
                    // method would return the next row.
                    doNext();

                    // And return the bean
                    return bean;

                } else {
                    throw new NoSuchElementException();
                }
            } catch (NoSuchElementException ex) {
                throw ex;
            } catch (Throwable t) {
                throw new SQLExecutionException(t);
            }
        }

        @Override
        public void remove() {
            // Removal is not supported
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Creates result set iterator. Used internally as a helper method to
     * construct a {@link Iterator} for {@link Query#execute(Class)} and
     * {@link Query#iterator(Class)} methods (and their variations).
     *
     * @param <T>
     *            returned type
     * @param returnedClass
     *            returned class
     * @param returnColumnName
     *            a column name from result set, which value is to be returned
     * @param maxFetchSize
     *            maximum fetch size - callers should set this value depending
     *            on expected / needed size of the result set
     * @return result set iterator
     */
    private <T> Iterator<T> createIterator(final Class<T> returnedClass, final String returnColumnName, final int maxFetchSize) {

        CallableStatement stmt = null;
        ResultSet rs = null;

        try {

            stmt = getConnection().prepareCall(getSql());

            // Set parameters
            for (String parameterName : queryMetaData.getInputParameterNames()) {
                InputParameter parameter = queryMetaData.getInputParameter(parameterName);
                bindInputParameter(stmt, parameter);
            }

            if (maxFetchSize > 0) {
                stmt.setFetchSize(maxFetchSize);
            } else {
                stmt.setFetchSize(Config.getQueryFetchSize());
            }

            rs = stmt.executeQuery();

            Iterator<T> iterator = new Iterator<T>(stmt, rs, returnedClass, returnColumnName);

            // Observe this iterator for cases when users do not call close at the end.
            ReferenceMonitor.observe(iterator,
                JDBCUtils.createFinalizer(rs),
                JDBCUtils.createFinalizer(stmt));

            return iterator;

        } catch (Throwable t) {
            throw new SQLExecutionException(t);
        }
    }

    /**
     * Closes iterator.
     * @param iterator iterator to close
     */
    private void closeIterator(final Iterator<?> iterator) {
        if (iterator != null) {
            iterator.close();
        }
    }
}
